﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using Kokomo.Environment;
using Kokomo.Environment.Commanding;
using Kokomo.Environment.Documents;
using Kokomo.Environment.Services;
using Kokomo.Mirror.CliMetadata;
using System.Windows;

namespace Kokomo.PeInspector.Reflection
{
	[ViewType(typeof(AssemblyListView))]
	class AssemblyListDocumentViewModel :
		AssemblyAnalyzerDocumentViewModel,
		IAssemblyListHost
	{
		public AssemblyListDocumentViewModel(
			AssemblyList assemblyList
			)
			: base(null)
		{
			if (assemblyList == null) throw new ArgumentNullException("assemblyList");
			if (!assemblyList.IsAttached) throw new ArgumentException("assemblyList must be attached.", "assemblyList");

			this.CanSave = true;
			this.PossibleFileTypes = new FileType[] { AssemblyAnalyzerEditor.AssemblyListFileType };

			this.LoadContext = assemblyList.LoadContext;

			this.AssemblyList = assemblyList;
			this._assemblyIndexTable = new CachedLookupTable<string, AssemblyIndex>(this.CreateAssemblyIndex);

			this.DependencyIndex = new ResidentDependencyIndex(
				this
				);

			this.LoadFrameworkAssemblyCommand = new DelegatedCommand(this.LoadFrameworkAssembly);
			this.LoadAssemblyCommand = new DelegatedCommand(this.LoadAssembly);
			this.RemoveAssemblyCommand = new ParameterizedCommand(this.RemoveAssembly);

			this._favorites = new ObservableCollection<TreeNodeViewModel>();
			this.AddFavoriteCommand = new ParameterizedCommand(this.AddFavorite);

			this.Search = new AssemblyListSearchViewModel(this.AssemblyList.Assemblies, this.GetAssemblyIndex, this.NavigateToCommand);

			AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AppDomain_AssemblyResolve);
			AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += new ResolveEventHandler(AppDomain_AssemblyResolve);

			this.AssemblySearchPaths = new List<string>();

			this.EditorMenuItemViewModels = new MenuItemViewModel[] {
				new MenuViewModel(UiString.FromString(Messages.AssemblyMenuTitle), new MenuItemViewModel[] {
					new MenuCommandItemViewModel(new CommandViewModel(this.LoadFrameworkAssemblyCommand, UiString.FromString(Messages.LoadFrameworkAssemblyMenuTitle))),
					new MenuCommandItemViewModel(new CommandViewModel(this.LoadAssemblyCommand, UiString.FromString(Messages.LoadAssemblyMenuTitle)))
				}),
			};

			this.AssemblyNodes = new DispatcherProjectedList<TreeNodeViewModel, Assembly>(
				assemblyList.Assemblies,
				this.CreateAssemblyNode,
				Application.Current.Dispatcher
				);
		}

		public IAssemblyLoadContext LoadContext { get; private set; }
		public IList<TreeNodeViewModel> AssemblyNodes { get; private set; }

		private TreeNodeViewModel _selectedAssemblyNode;
		public TreeNodeViewModel SelectedAssemblyNode
		{
			get { return this._selectedAssemblyNode; }
			set
			{
				this._selectedAssemblyNode = value;
				this.NavigateTo(value.Target);
				this.OnPropertyChanged("SelectedAssemblyNode");
			}
		}

		protected override void OnSaving(string fileName)
		{
			AssemblyList list = this.AssemblyList;
			List<AssemblyMemberRef> favorites = new List<AssemblyMemberRef>(this.Favorites.Count);
			foreach (var favoriteNode in this.Favorites)
			{
				AssemblyMemberRef memberRef = null;
				Assembly assembly = favoriteNode.Target as Assembly;
				if (assembly != null)
				{
					memberRef = new AssemblyMemberRef() { AssemblyDisplayName = assembly.FullName };
				}
				else
				{
					MemberInfo member = favoriteNode.Target as MemberInfo;
					if (member != null)
					{
						memberRef = new AssemblyMemberRef()
						{
							AssemblyDisplayName = member.Module.Assembly.FullName,
							MemberToken = member.MetadataToken
						};
					}
				}

				if (memberRef != null)
				{
					favorites.Add(memberRef);
				}
			}
			list.Favorites = favorites.ToArray();

			XmlSerializer serializer = new XmlSerializer(typeof(AssemblyList));
			using (Stream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
			{
				serializer.Serialize(stream, list);
			}
		}

		#region Assemblies
		private string GetAssemblyPath(AssemblyName assemblyName)
		{
			string assemblyFileName = assemblyName.Name + ".dll";

			// Check search paths
			foreach (var searchPath in this.AssemblySearchPaths)
			{
				string assemblyPath = Path.Combine(searchPath, assemblyFileName);
				if (File.Exists(assemblyPath))
				{
					try
					{
						AssemblyName tentativeName = AssemblyName.GetAssemblyName(assemblyPath);
						//if (tentativeName.FullName == assemblyName.FullName)
						{
							return assemblyPath;
						}
					}
					catch { }
				}
			}

			return null;
		}

		private Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
		{
			AssemblyName unresolvedAssemblyName = new AssemblyName(args.Name);

			string assemblyPath = this.GetAssemblyPath(unresolvedAssemblyName);
			if (assemblyPath != null)
			{
				return this.LoadAssemblyFile(assemblyPath);
			}
			return null;
		}

		public AssemblyList AssemblyList { get; private set; }

		public DependencyIndex DependencyIndex { get; private set; }
		public List<string> AssemblySearchPaths { get; private set; }

		private CachedLookupTable<string, AssemblyIndex> _assemblyIndexTable;
		public AssemblyIndex GetAssemblyIndex(Assembly assembly)
		{
			if (assembly == null) throw new ArgumentNullException("assembly");

			return this._assemblyIndexTable[assembly.Location];
		}
		private AssemblyIndex CreateAssemblyIndex(string assemblyFileName)
		{
			Assembly assembly = this.LoadContext.LoadAssemblyFile(assemblyFileName);
			AssemblyIndex assemblyIndex = AssemblyIndex.Create(assembly, this.LoadContext);
			return assemblyIndex;
		}

		public ICommand LoadAssemblyCommand { get; private set; }
		private void LoadAssembly()
		{
			string[] fileNames = EnvironmentServices.UiService.ShowOpenFileDialog(new FileDialogOptions()
			{
				FileTypes = new FileType[] { AssemblyAnalyzerEditor.AssemblyFileType },
				Title = UiString.FromString(Messages.LoadAssemblyTitle),
				AllowMultiple = true
			}).FileNames;

			if (fileNames != null)
			{
				LoadAssemblyFiles(fileNames);
			}
		}

		private void LoadAssemblyFiles(string[] fileNames)
		{
			foreach (var fileName in fileNames)
				LoadAssemblyFile(fileName);
		}

		private Assembly LoadAssemblyFile(string fileName)
		{
			Assembly assembly = this.LoadContext.LoadAssemblyFile(fileName);
			this.AssemblyList.AddAssembly(assembly);
			return assembly;
		}

		public ICommand LoadFrameworkAssemblyCommand { get; private set; }
		private void LoadFrameworkAssembly()
		{
			string frameworkDirectory = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Windows), @"Microsoft.NET\Framework\");

			string[] fileNames = EnvironmentServices.UiService.ShowOpenFileDialog(new FileDialogOptions()
			{
				FileTypes = new FileType[] { AssemblyAnalyzerEditor.AssemblyFileType },
				Title = UiString.FromString(Messages.LoadFrameworkAssemblyTitle),
				InitialDirectory = frameworkDirectory,
				AllowMultiple = true
			}).FileNames;

			if (fileNames != null)
			{
				LoadAssemblyFiles(fileNames);
			}
		}

		public ICommand RemoveAssemblyCommand { get; private set; }
		private void RemoveAssembly(object arg)
		{
			Assembly assembly = arg as Assembly;
			if (assembly == null)
			{
				TreeNodeViewModel assemblyNode = arg as TreeNodeViewModel;
				if (assemblyNode == null) return;
				assembly = (Assembly)assemblyNode.Target;
			}
			if (assembly == null) return;

			lock (this.AssemblyList)
			{
				this.AssemblyList.Assemblies.Remove(assembly);
			}
		}
		#endregion

		#region Type Tree
		private TreeNodeViewModel CreateAssemblyNode(Assembly assembly)
		{
			//if (Application.Current.Dispatcher.Thread != Thread.CurrentThread)
			//{
			//    return (TreeNodeViewModel)Application.Current.Dispatcher.Invoke(
			//        (Func<Assembly, TreeNodeViewModel>)this.CreateAssemblyNode,
			//        assembly
			//        );
			//}
			//else
			{
				MemberViewInfo viewInfo = MemberViewInfo.GetMemberViewInfo(assembly);
				UiString heading = viewInfo.Heading;
				ImageSource iconSource;
				if (assembly.EntryPoint != null)
				{
					iconSource = AssemblyAnalyzerEditor.GetIconForModule(assembly.Location);
				}
				else
				{
					iconSource = AssemblyAnalyzerEditor.MetadataIconProvider.GetImage(MetadataIconKeys.Assembly);
				}
				Func<ProgressInfo, IEnumerable<TreeNodeViewModel>> itemsSourceDelegate = (progress) => this.GetAssemblyNodeItemsSource(assembly);

				TreeNodeViewModel assemblyNode = new TreeNodeViewModel(iconSource, heading, assembly)
				{
					ItemsSourceDelegate = itemsSourceDelegate,
					ContextMenuItems = new MenuItemViewModel[] {
					new MenuCommandItemViewModel(new CommandViewModel(this.AddFavoriteCommand, UiString.FromString(Messages.AddFavoriteCommandText)) {Argument=assembly}),
					new MenuCommandItemViewModel(new CommandViewModel(this.RemoveAssemblyCommand, UiString.FromString(Messages.RemoveFromListTitle)) { Argument=assembly}),
				}
				};

				string assemblyDirectory = Path.GetDirectoryName(assembly.Location);
				lock (this.AssemblySearchPaths)
				{
					if (!this.AssemblySearchPaths.Contains(assemblyDirectory))
					{
						this.AssemblySearchPaths.Add(assemblyDirectory);
					}
				}
				this.DependencyIndex.AddAssembly(assembly);

				return assemblyNode;
			}
		}
		private IEnumerable<TreeNodeViewModel> GetAssemblyNodeItemsSource(Assembly assembly)
		{
			AssemblyIndex index = this.GetAssemblyIndex(assembly);
			var q = index.NamespaceGroups.Select(n => this.CreateNamespaceNode(n));

			return q;
		}
		private TreeNodeViewModel CreateNamespaceNode(NamespaceScope namespaceScope)
		{
			string key = namespaceScope.Namespace;
			string heading = string.IsNullOrEmpty(key) ? "-" : key;
			ImageSource iconSource = AssemblyAnalyzerEditor.MetadataIconProvider.GetImage(MetadataIconKeys.Namespace);
			Func<ProgressInfo, IEnumerable<TreeNodeViewModel>> itemsSourceDelegate = (progress) => GetNamespaceNodeItemsSource(namespaceScope.Types);

			return new TreeNodeViewModel(iconSource, UiString.FromString(heading), namespaceScope) { ItemsSourceDelegate = itemsSourceDelegate };
		}
		private IEnumerable<TreeNodeViewModel> GetNamespaceNodeItemsSource(IEnumerable<Type> types)
		{
			var q = types.Select(type => this.CreateMemberNode(type));

			return q;
		}
		#endregion


		#region Search
		public AssemblyListSearchViewModel Search { get; private set; }
		#endregion

		#region Favorites
		private TreeNodeViewModel _selectedFavorite;
		public TreeNodeViewModel SelectedFavorite
		{
			get { return this._selectedFavorite; }
			set
			{
				this._selectedFavorite = value;
				this.OnSelectedFavoriteChanged();
			}
		}
		protected virtual void OnSelectedFavoriteChanged()
		{
			if (this._selectedFavorite != null) this.NavigateTo(this._selectedFavorite.Target);

			this.OnPropertyChanged("SelectedFavorite");
		}

		private ObservableCollection<TreeNodeViewModel> _favorites;
		public IList<TreeNodeViewModel> Favorites
		{
			get { return this._favorites; }
		}

		public ICommand AddFavoriteCommand { get; private set; }
		private void AddFavorite(object arg)
		{
			Assembly assembly = arg as Assembly;
			if (assembly != null)
			{
				this.AddFavorite(assembly);
			}
			else
			{
				MemberInfo member = arg as MemberInfo;
				if (member != null)
				{
					this.AddFavorite(member);
				}
			}
		}
		public void AddFavorite(Assembly assembly)
		{
			if (assembly == null) throw new ArgumentNullException("assembly");

			this.Favorites.Add(this.CreateAssemblyNode(assembly));
		}
		public void AddFavorite(MemberInfo member)
		{
			if (member == null) throw new ArgumentNullException("member");

			this.Favorites.Add(this.CreateMemberNode(member));
		}
		#endregion
	}
}
